Parse and stringify JSON with comments. It will retain comments even after saved!
- Parse JSON strings with comments into JavaScript objects and MAINTAIN comments
- supports comments everywhere, yes, EVERYWHERE in a JSON file, eventually 😆
- fixes the known issue about comments inside arrays.
- Stringify the objects into JSON strings with comments if there are
The usage of comment-json
is exactly the same as the vanilla JSON
object.
Table of Contents
Why?
There are many other libraries that can deal with JSON with comments, such as json5, or strip-json-comments, but none of them can stringify the parsed object and return back a JSON string the same as the original content.
Imagine that if the user settings are saved in ${library}.json
, and the user has written a lot of comments to improve readability. If the library library
need to modify the user setting, such as modifying some property values and adding new fields, and if the library uses json5
to read the settings, all comments will disappear after modified which will drive people insane.
So, if you want to parse a JSON string with comments, modify it, then save it back, comment-json
is your must choice!
How?
comment-json
parse JSON strings with comments and save comment tokens into symbol properties.
For JSON array with comments, comment-json
extends the vanilla Array
object into CommentArray
whose instances could handle comments changes even after a comment array is modified.
Install
$ npm i comment-json
For TypeScript developers, @types/comment-json
could be used
Since 2.4.1
, comment-json
contains typescript declarations, so you might as well remove @types/comment-json
.
Usage
package.json:
{
"name": "comment-json"
}
const {
parse,
stringify,
assign
} = require('comment-json')
const fs = require('fs')
const obj = parse(fs.readFileSync('package.json').toString())
console.log(obj.name)
stringify(obj, null, 2)
Sort keys
It is a common use case to sort the keys of a JSON file
const parsed = parse(`{
// b
"b": 2,
// a
"a": 1
}`)
const sorted = assign(
{},
parsed,
Object.keys(parsed).sort()
)
console.log(stringify(sorted, null, 2))
For details about assign
, see here.
parse()
parse(text, reviver? = null, remove_comments? = false)
: object | string | number | boolean | null
- text
string
The string to parse as JSON. See the JSON object for a description of JSON syntax. - reviver?
Function() | null
Default to null
. It acts the same as the second parameter of JSON.parse
. If a function, prescribes how the value originally produced by parsing is transformed, before being returned. - remove_comments?
boolean = false
If true, the comments won't be maintained, which is often used when we want to get a clean object.
Returns CommentJSONValue
(object | string | number | boolean | null
) corresponding to the given JSON text.
If the content
is:
{
"foo" :
1
,
"bar": [
"baz"
,
"quux"
]
}
const {inspect} = require('util')
const parsed = parse(content)
console.log(
inspect(parsed, {
showHidden: true
})
)
console.log(Object.keys(parsed))
console.log(stringify(parsed, null, 2))
And the value of parsed
will be:
{
[Symbol.for('before-all')]: [{
type: 'BlockComment',
value: '\n before-all\n ',
inline: false,
loc: {
start: {
line: 1,
column: 0
},
end: {
line: 3,
column: 3
}
}
}, {
type: 'LineComment',
value: ' before-all',
inline: false,
loc: ...
}],
...
[Symbol.for('after-prop:foo')]: [{
type: 'BlockComment',
value: ' after-prop:foo ',
inline: true,
loc: ...
}],
foo: 1,
bar: [
"baz",
"quux",
[Symbol.for('after-value:0')]: [{
type: 'LineComment',
value: ' after-value:0',
inline: true,
loc: ...
}, ...],
...
]
}
There are EIGHT kinds of symbol properties:
Symbol.for('before-all')
Symbol.for('before')
Symbol.for(`before:${prop}`)
Symbol.for(`after-prop:${prop}`)
Symbol.for(`after-colon:${prop}`)
Symbol.for(`after-value:${prop}`)
Symbol.for(`after:${prop}`)
Symbol.for('after-all')
And the value of each symbol property is an array of CommentToken
interface CommentToken {
type: 'BlockComment' | 'LineComment'
value: string
inline: boolean
loc: CommentLocation
}
interface CommentLocation {
start: Location
end: Location
}
interface Location {
line: number
column: number
}
comment-json
provides a symbol
-type called CommentSymbol
which can be used for querying comments.
Furthermore, a type CommentDescriptor
is provided for enforcing properly formatted symbol names:
import {
CommentDescriptor, CommentSymbol, parse, CommentArray
} from 'comment-json'
const parsed = parse(`{ /* test */ "foo": "bar" }`)
const symbolName: CommentDescriptor = 'before:foo'
console.log((parsed as CommentArray<string>)[Symbol.for(symbolName) as CommentSymbol][0].value)
In this example, casting to Symbol.for(symbolName)
to CommentSymbol
is mandatory.
Otherwise, TypeScript won't detect that you're trying to query comments.
console.log(parse(content, null, true))
And the result will be:
{
foo: 1,
bar: [
"baz",
"quux"
]
}
Special cases
const parsed = parse(`
// comment
1
`)
console.log(parsed === 1)
If we parse a JSON of primative type with remove_comments:false
, then the return value of parse()
will be of object type.
The value of parsed
is equivalent to:
const parsed = new Number(1)
parsed[Symbol.for('before-all')] = [{
type: 'LineComment',
value: ' comment',
inline: false,
loc: ...
}]
Which is similar for:
For example
const parsed = parse(`
"foo" /* comment */
`)
Which is equivalent to
const parsed = new String('foo')
parsed[Symbol.for('after-all')] = [{
type: 'BlockComment',
value: ' comment ',
inline: true,
loc: ...
}]
But there is one exception:
const parsed = parse(`
// comment
null
`)
console.log(parsed === null)
stringify()
stringify(object: any, replacer?, space?): string
The arguments are the same as the vanilla JSON.stringify
.
And it does the similar thing as the vanilla one, but also deal with extra properties and convert them into comments.
console.log(stringify(parsed, null, 2))
space
If space is not specified, or the space is an empty string, the result of stringify()
will have no comments.
For the case above:
console.log(stringify(result))
console.log(stringify(result, null, 2))
assign(target: object, source?: object, keys?: Array)
- target
object
the target object - source?
object
the source object. This parameter is optional but it is silly to not pass this argument. - keys?
Array<string>
If not specified, all enumerable own properties of source
will be used.
This method is used to copy the enumerable own properties and their corresponding comment symbol properties to the target object.
const parsed = parse(`// before all
{
// This is a comment
"foo": "bar"
}`)
const obj = assign({
bar: 'baz'
}, parsed)
stringify(obj, null, 2)
Special cases about keys
But if argument keys
is specified and is not empty, then comment before all
, which belongs to no properties, will NOT be copied.
const obj = assign({
bar: 'baz'
}, parsed, ['foo'])
stringify(obj, null, 2)
Specifying the argument keys
as an empty array indicates that it will only copy non-property symbols of comments
const obj = assign({
bar: 'baz'
}, parsed, [])
stringify(obj, null, 2)
Non-property symbols include:
Symbol.for('before-all')
Symbol.for('before')
Symbol.for('after-all')
Advanced Section
All arrays of the parsed object are CommentArray
s.
The constructor of CommentArray
could be accessed by:
const {CommentArray} = require('comment-json')
If we modify a comment array, its comment symbol properties could be handled automatically.
const parsed = parse(`{
"foo": [
// bar
"bar",
// baz,
"baz"
]
}`)
parsed.foo.unshift('qux')
stringify(parsed, null, 2)
Oh yeah! 😆
But pay attention, if you reassign the property of a comment array with a normal array, all comments will be gone:
parsed.foo = ['quux'].concat(parsed.foo)
stringify(parsed, null, 2)
Instead, we should:
parsed.foo = new CommentArray('quux').concat(parsed.foo)
stringify(parsed, null, 2)
Special Cases about Trailing Comma
If we have a JSON string str
{
"foo": "bar",
}
const stringified = stringify(parse(str), null, 2)
console.log(stringified)
And it will print:
{
"foo": "bar"
}
License
MIT
Change Logs
See releases